昨天我們輕鬆地介紹兩個與進度有關的元件,今天讓我們稍微精實一點,來介紹一下寫程式多於寫HTML的Dialog,不管在不在SPA架構,Dialog都是經典且極為重要的元件,也因此我們會比較多的時間來介紹,好讓讀著們能完全的掌控Angular Material中的Dialog使用方式!
在Material Design的Dialogs設計指南中,Dialog的作用是用來提醒使用者需要進行的一些特定工作,同時可能包含了重要的提示訊息,或是需要做一些決定等等。因此我們會有非常多機會在裡面放是表單元件,或是特性訊息等等。
Dialog的主要幾個常見用途如下:
Dialog可以說是很基礎的元件,也可以說是讓畫面呈現變得更加有立體感的關鍵,例如我們過去介紹的Datepicker、Select、Menu等等,都可以說是Dialog的一種應用結果。
要使用Dialog,當然我們必須加入MatDialogModule
,接著我們就可以來設計一個簡單的Dialog元件。
Dialog不像是其他Angular Material元件,只要單純的使用即可,需要一些比較複雜的動作,但其實也不是說多困難,讓我們一步一步來說明:
我們先單純建立一個元件AddPostDialogComponent
元件,不改變任何內容
ng g c dashboard/blog/add-post-dialog
接著用一個按鈕,希望這個按鈕按下後可以顯示Dialog
<button mat-raised-button color="primary" (click)="showAddPostDialog()">新增文章</button>
在對應的component.ts中,注入MatDialog
這個Service
import { MatDialog } from '@angular/material';
export class BlogComponent {
constructor(public dialog: MatDialog) {}
}
使用這個MatDialog
的實體,打開Dialog
showAddPostDialog() {
this.dialog.open(AddPostDialogComponent);
}
由於這種方式是動態產生元件的,因此我們需要在所屬Module中的entryComponents
中加入要產生的component
@NgModule({
...
entryComponents: [AddPostDialogComponent]
})
export class DashboardModule {}
雖然看起來步驟比較多,但其實只有兩個重點:
MatDialog
實體把它打開這種步驟理解並習慣了就很快囉。接著就讓我們直接來看看結果吧!
有沒有很感動啊!一個基本的Dialog就浮現在我們面前啦!我們這時候可以透過點擊灰底(backdrop)的部分來關閉dialog。
在dialog中這個灰底的部分稱為
backdrop
,我找不到比較好的翻譯,因此之後依舊會直接使用backdrop來稱呼它。
在MatDialogModule
中,定義了幾個重要的directives,這些directives可以幫助我們豐富dialog裡面的內容,同時還能夠減少一些不必要的程式碼,讓我們簡單來介紹一下:
代表的是一個dialog的標題部分,儘管因為dialog的內容高度太長而造成捲動,依然會固定在整個dialog的最上方。
代表一個dialog的內文部分,當內容長度超過dialog可以容納的高度時,就會變成可以捲動的模式。
用來放置行動按鈕的區塊,呈現位置剛好與mat-dialog-title
相反,會固定在畫面的最下方,我們會在這裡放置一些如確認、取消的按鈕。
只允許在button上使用的directive,這個directive會使得button變成一個可以關閉目前dialog的按鈕。
接下來就讓我們把一個簡單的新增文章頁面加入,並透過剛剛介紹的directives讓整個dialog更加完整吧!
<h2 mat-dialog-title>
新增部落格文章
</h2>
<mat-dialog-content class="post-form">
<mat-form-field>
<input matInput placeholder="標題" />
</mat-form-field>
<mat-form-field>
<textarea matInput placeholder="文章內容" rows="3"></textarea>
</mat-form-field>
<p>條款01</p>
<p>條款02</p>
...
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button color="primary">發表</button>
<button mat-button mat-dialog-close color="warn">取消</button>
</mat-dialog-actions>
再來看看結果:
一個標準的dialog就誕生啦!除了依照title、content和actions切割空間之外,按下取消的按鈕就能夠關閉dialog,另外當高度超過可以自動延展的範圍(Angular Material中的dialog設定為65vh
)時,就會變成可以捲動的狀態。
讀者有興趣可以實際試玩看看這個dialog的效果,可以看到以下幾個亮點:
tab / shift + tab
切換focus狀態時,永遠不會跳出dialog的範圍,只會在dialog內移動。ESC
鍵也可以。以上特色我們在未來介紹Angular CDK時,都可以透過Angular CDK來幫助我們在自己設計的元件中達到一樣的功能!在這邊就先不多做說明囉。
###關於MatDialog Service
在一開始介紹如何打開一個dialog時,我們注入了MatDialog
這個Service,接下來我們來詳細介紹一下這個Service的屬性及方法:
MatDialog有3個屬性:
afterAllClosed
:Observable<void>
,會在所有畫面上的dialog都被關閉時,才會觸發的一個Observable,從這樣的說明應該可以發現:沒錯,Dialog是可以開多個的!只需要在任何時候使用MatDialog.open()
方法即可afterOpen
:Observable<MatDialogRef<any>>
,每當一個dialog開啟時,就會觸發一次,並告知目前開啟的dialogopenDialogs
:MatDialogRef<any>[]
,單純的紀錄目前所有開啟中的dialog。簡單的範例如下:
this.dialog.afterAllClosed.subscribe(() => {
console.log('目前已經沒有dialog了');
});
this.dialog.afterOpen.subscribe((dialogRef: MatDialogRef<any>) => {
console.log(`新的dialog已開啟:${dialogRef.id}`);
console.log(`目前已開啟 ${this.dialog.openDialogs.length} 個dialog了`);
});
開啟多的dialog的程式就不多做說明了,只需要呼叫注入的MatDialog
的open()
方法,就會自然而然地開一個
新的dialog。
我們直接來看看log顯示的結果:
MatDialog有3個方法,可以讓我們自由自在地控制dialog:
closeAll
:顧名思義,就是關閉所有的dialog
getDialogById
:每個dialog都有他自己的id,當然我們也可以自行指定id,不管是用哪種方式,只要有id,我們就能在任何時候使用getDialogById(id)
,來取得某個dialog,如果無法取得的話,會回傳undefined
。
open
:最重要的一個方法,包含2個參數
componentOrTemplateRef:ComponentType<T> | TemplateRef<T>
:必填,要顯示的dialog,我們可以傳入componet或是一個templateRef。
<ng-template #dialogByTemplate>
...
</ng-template>
在程式中可以直接將這個<ng-template>
當作dialog顯示,可以減少設計太多的component,反而難以管理。
@ViewChildren('dialogByTemplate') dialogByTemplate;
open() {
this.dialog.open(this.dialogByTemplate);
}
config?:MatDialogConfig<D>
:非必填,用來設定一些顯示的細節。我們稍後會直接針對這個MatDialogConfig
型別做說明。
我們可以透過MatDialogConfig
型別設定一些dialog打開時的細節,由於屬性眾多,以下挑幾個個人覺得重要的來介紹:
超重要,我們不可能永遠只是單純地打開一個dialog,一定會有需要傳入一些資訊的時候,這時我們就可以使用data參數來傳入一些資訊,如下:
doPost() {
this.dialog.open(AddPostConfirmDialogComponent, {
data: {
title: this.title
}
});
}
而在dialog內,我們可以使用@Inject(MAT_DIALOG_DATA)
來注入需要的資訊
@Component()
export class AddPostConfirmDialogComponent implements OnInit {
get title(){
return this.data.title;
}
constructor(@Inject(MAT_DIALOG_DATA) private data: any) {
console.log(data.title);
}
}
成果如下:
當dialog打開時,是否要自動focus在第一個控制項
我們可以為每個dialog自定一個唯一的id
我們可以使用height
、width
、minHeight
、minWidth
、maxHeight
和maxWidth
來設定dialog的尺寸資訊,除了height
和width
一定要用字串表示外,其他屬性可以給予數值,當給予數值而非字串時,預設的單位為px
。
是否要使用一個灰色的底來隔絕dialog與下面的畫面,也就是backdrop,如果設定為false
則依然可以和dialog後面的元件互動。
showAddPostDialog() {
this.dialog.open(AddPostDialogComponent, {
hasBackdrop: false
});
}
結果如下:
可以設定backdrop的樣式,不太常使用,若對灰色底不滿意時,可以進行調整,預設樣式如下:
background: rgba(0,0,0,.6);
可以設定top
、bottom
、left
和right
,來決定dialog顯示的位置。
預設情況下我們可以使用ESC
鍵關閉dialog,透過設定disableClose
為true
,可以取消這個功能,但要注意可能影響使用者的使用經驗。
所有的dialog開啟後,都會產生一個對應的MatDialogRef<T>
,其中的T
代表實際產生的component或templateRef,取得這個DialogRef的方式很多,主要有
使用MatDialog
的open()
時,回傳的值,例如以下程式範例,可以透過取得開啟對應component的MatDialogRef,來處理原來元件的事件:
doPost() {
const confirmDialogRef = this.dialog.open(AddPostConfirmDialogComponent, {
data: {
title: this.title
}
});
// doConfirm是AddPostConfirmDialogComponent中的事件(EventEmitter)
// 透過componentInstance取得AddPostConfirmDialogComponent產生的實體
confirmDialogRef.componentInstance.doConfirm.subscribe(() => {
console.log('開啟的dialog按下確認按鈕了');
});
}
使用MatDialog
的getDialogById
取得
在我們要當作dialog的component中,注入取得
@Component()
export class AddPostDialogComponent {
constructor(private dialogRef: MatDialogRef<AddPostDialogComponent>)
move() {
this.dialogRef.updatePosition({
top: '0',
left: '0'
});
}
}
結果如下:
MatDialogRef許多方法都跟前面介紹過的類似,就不多作介紹,有興趣的可以再去仔細看看dialog的API內容。
今天我們用了非常多的文字及程式碼來介紹dialog這個功能,要建立並產生一個dialog放到畫面上本身並不是一件困難的事情,但更多時候我們需要的是dialog的細節設定,這時候就必須仰賴程式碼的幫助了。
所以今天的內容會比較多偏向程式碼的方面,來說明所有相關的component、directive和service的屬性方法等等,並透過這些來調整dialog的顯示。
Dialog是前端非常經典的議題,也是SPA架構下不可或缺的一個環節,在Material Design中更是應用範圍極廣,如果能好好善用dialog,在設計互動性較高的介面時,也能夠更加靈活,讓使用者經驗大幅提高哩!
本日的程式碼GitHub:https://github.com/wellwind/it-ironman-demo-angular-material/tree/day-19-dialog
分支:day-19-dialog
你好:
想請教一個問題,是否可在 dialog 打開後,尚可點擊 gialog 外的按鈕,比如是 header 的 button,因為我想做一個截截圖功能,萬一使用者覺得打開 dialog 有問題,可以透過系統內建的截圖功能直接回報問題,感謝你
dialog 有一個 hasBackdrop 可以設定,設定後就可以按 dialog 之下的元素,若希望按其他元素不要被關掉,可以設定 disableClose
可以參考 dialog 可以用的屬性
https://material.angular.io/components/dialog/api#MatDialogConfig
謝謝!但這樣有一個缺點,萬一使用者按到轉換另一個頁面時,該 dialog 還是停留著,有點唐突
這也很好解決, 在頁面元件的 ngOnDestory()
時關掉就好了
如何改變在同一頁面上開啟的多個Dialog 的z-index?
例如有2個Dialog, 分別為A和B. 最初A置於B後面, A的一部分的內容被B遮擋. 當點擊A後, A移到B的前面.
能否實現此功能? 我查看了angular material 的doc都沒有提到z-index.
在拿到 MatDialogRef 後可以使用 addPanelClass
和 removePanelClass
來加入/移除樣式。
因此可以訂一個類似移動到最上面的 CSS
.forceFront {
z-index: 9999;
}
之後在適當的時機加入或移除此樣式即可。
這個功能在 Angular Material 7 之後才有。
我發現使用addPanelClass/removePanelClass 只能改到內層的Panel, Dialog的前後次序仍被外層的z-index限制.
在網上找到了一個方法, 取得dialog 的overlay-container
const overlayWrapper = this.dialogRef['_overlayRef'].hostElement;
overlayWrapper.classList.add('front-window');
感謝提供,這是個雖然不美但可以用的作法,希望之後 Angular Material 可以考慮把 overlayRef 轉成公開屬性 XDD